#include "DataFilter.h"
#include "GLTFParser.h"
#include "GeoUtil.h"

void DataFilter::slotCameraMatrix(const Mat& viewMatrix, const Mat& projectionMatrix) {
	m_viewMatrix = viewMatrix;
	m_projectionMatrix = projectionMatrix;

	const auto& SkylineY = Config::View3D_SkylineFromTopPx;
	auto view3DSkylineLeft = mapTo3DXZPlane(0, SkylineY);
	auto view3DSkylineRight = mapTo3DXZPlane(W3D, SkylineY);
	auto view3DBottomLeft = mapTo3DXZPlane(0, H3D);
	auto view3DBottomRight = mapTo3DXZPlane(W3D, H3D);

	m_3DRenderAreaMap.insert(SkyLeft, view3DSkylineLeft);
	m_3DRenderAreaMap.insert(SkyRight, view3DSkylineRight);
	m_3DRenderAreaMap.insert(BottomLeft, view3DBottomLeft);
	m_3DRenderAreaMap.insert(BottomRight, view3DBottomRight);

	LogI << "************************************************************************************";
	LogI << "Skyline-Left (x:" << view3DSkylineLeft.x << ",z:" << view3DSkylineLeft.z << ")"
		 << ", Skyline-Right (x:" << view3DSkylineRight.x << ",z:" << view3DSkylineRight.z << ")";
	LogI << "Bottom-Left (x:" << view3DBottomLeft.x << ",z:" << view3DBottomLeft.z << ")"
		 << ", Bottom-Right (x:" << view3DBottomRight.x << ",z:" << view3DBottomRight.z << ")";

	s_2dScalerFunc = [](float z)->float {
		float limitStartZ = get()->bottomZ() + 0.1;
		float limitEndZ = get()->skylineZ() - 0.1;
		float t = (z - limitStartZ) / (limitEndZ - limitStartZ);
		t = fmaxf(0.0f, fminf(1.0f, t));// limit t range:[0, 1]
		float scale = 1.0 - 0.7 * t;//scale: [zStart, zEnd] <=> [1, 0.3]
		return scale;
	};
}

/**
 *@warning KT: id = 0 is filterd!
 *@return true: data is valid; false: data is invalid
 */
bool DataFilter::checkValid(const ModelArg& modelArg) {
	if (ModelType::INVALID == modelArg.type)
		return false;
	if (Proj::isKT() || Proj::isSQZK()) {
		if (!modelArg.identified || modelArg.probability <= 0 || modelArg.ID <= 0 || modelArg.width <= 0 || modelArg.length <= 0)
			return false;
	}
	if (isOutOfRenderArea(modelArg.pos))
		return false;
	return true;
}

bool DataFilter::checkValid(LineArg& lineArg) {
	if (LineStyle::INVALID == lineArg.style || !lineArg.color.isValid())
		return false;
	if (!lineArg.identified || lineArg.probability <= 0 || !lineArg.isPointsData() && lineArg.eq.zStart <= lineArg.eq.zEnd)
		return false;
	if (!lineArg.isPointsData() && isOutOfRenderArea(lineArg.eq))
		return false;
	recalculateEqStartEnd(lineArg.eq);
	if (!lineArg.eq.isValid())
		return false;
	return true;
}

bool DataFilter::isOutOfRenderArea(const P& pos) {
	if (pos.z < skyLeftP().z || pos.z > bottomLeftP().z)
		return true;
	if (pos.x < 0)
		return GeoUtil::checkIfOnRayCCWSweepingSide(pos, bottomLeftP(), skyLeftP());
	else
		return !GeoUtil::checkIfOnRayCCWSweepingSide(pos, bottomRightP(), skyRightP());
}

bool DataFilter::isOutOfRenderArea(const LineEq& eq) {
	if (eq.zStart <= skylineZ() || eq.zEnd >= bottomZ())
		return true;
	return false;
}

/** @note for points data, it may lead to invalid points data(points.size() <=2), because of out-of-range points' removal! */
void DataFilter::recalculateEqStartEnd(LineEq& eq) {// call it before lineEq datas to be used
	bool IsCutOffByStart = Config::Line_CutOff_ByEqStart;
	bool IsCutOffByEnd = Config::Line_CutOff_ByEqEnd;
	const auto& ZhicheInfo = GLTFParser::info(ModelType::Zhiche);
	const auto ZhicheFrontZ = -(ZhicheInfo.length - ZhicheInfo.maxZ);
	const auto ZBottom = bottomZ();
	const auto ZSky = skylineZ();

	if (eq.isPointsData()) {
		/**@important make z-asc, and no duplicated z-values(otherwise,not strict z monotonic increase!) */
		auto& points = eq.points;
		std::sort(points.begin(), points.end(), [](const P& p1, const P& p2) { return p1.z < p2.z; });// make order by z-increasing
		auto zEquals = [](const P& a, const P& b) { return a.z == b.z; };
		auto it = std::unique(points.begin(), points.end(), zEquals);// make z no-duplicate, just keep first one
		points.erase(it, points.end());

		if (points.empty()) return;// .back()/.front() throw error if points is empty!

		eq.zStart = points.back().z;
		eq.zEnd = points.front().z;
		eq.C << -points.back().x;// C's dir is opposite
	}

	if (eq.zStart < ZhicheFrontZ)// should ignore cutoff-status, always cutoff
		IsCutOffByStart = true;

	const float VOFFSET = 0.1;
	if (!IsCutOffByStart || eq.zStart > ZBottom)
		eq.zStart = ZBottom + VOFFSET;
	if (!IsCutOffByEnd || eq.zEnd < ZSky)
		eq.zEnd = ZSky;

	if (eq.isPointsData()) {
		auto& points = eq.points;
		int endRmNum = 0, startRmNum = 0;
		P closestZStartP, closestZEndP;
		for (int i = points.size() - 1; i >= 0; i--) {
			if (points[i].z > eq.zStart) {
				++startRmNum;
				closestZStartP = points[i];
			}
			else break;
		}
		for (const auto& p : points) {
			if (p.z < eq.zEnd) {
				++endRmNum;
				closestZEndP = p;
			}
			else break;
		}
		points.erase(points.begin(), points.begin() + endRmNum);
		points.erase(points.end() - startRmNum, points.end());

		if (!points.empty()) {// at lease one point between zStart and zEnd
			/**@note avoid lack of important points when points is little, lead to total points num less than 3 (because of erase some out range points)*/
			if (endRmNum > 0 && points.front().z > eq.zEnd)// note: the second condition is to avoid duplicate!
				points.insert(points.begin(), P(points.front().x, eq.zEnd));
			if (startRmNum > 0 && points.back().z < eq.zStart)// note: the second condition is to avoid duplicate!
				points << P(points.back().x, eq.zStart);
		} else if (closestZStartP.isValid() && closestZEndP.isValid()) {// all points is below start or above end!
			auto closestLine = MakePair(closestZStartP, closestZEndP);
			auto zStartHorizonLine = MakePair(P(-100, eq.zStart), P(100, eq.zStart));
			auto zEndHorizonLine = MakePair(P(-100, eq.zEnd), P(100, eq.zEnd));
			P intersectStartP = GeoUtil::getLineIntersection(closestLine, zStartHorizonLine);
			P intersectEndP = GeoUtil::getLineIntersection(closestLine, zEndHorizonLine);
			points << intersectEndP << intersectStartP;
		}

		if (points.size() == 2) {// points size == 2 => add one to make valid!
			auto newMidP = (points[0] + points[1]) / 2;
			points.insert(points.begin() + 1, newMidP);
		}
	}
}

P DataFilter::mapTo3DXZPlane(int screenX, int screenY) {
	ASSERT(isInitialized(), "viewMatrix and projectionMatrix is not initialized!");
	if (Proj::isSSQK())
		return GeoUtil::mapToOrtho3DXZPlane(Vec2(screenX, screenY), CSize(W3D, H3D), m_viewMatrix, m_projectionMatrix);
	else
		return GeoUtil::mapTo3DXZPlane(Vec2(screenX, screenY), CSize(W3D, H3D), m_viewMatrix, m_projectionMatrix);
}